1 
2 /*
3 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
4 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
5 Authors: Marcelo S. N. Mancini
6 
7 	Copyright Marcelo S. N. Mancini 2018 - 2021.
8 Distributed under the CC BY-4.0 License.
9    (See accompanying file LICENSE.txt or copy at
10 	https://creativecommons.org/licenses/by/4.0/
11 */
12 module hip.game2d.sprite;
13 public import hip.api.renderer.texture;
14 public import hip.api.graphics.color;
15 public import hip.api.data.commons;
16 import hip.math.vector;
17 import hip.api;
18 import hip.game2d.renderer_data;
19 
20 /**
21 *   Encapsulates bunch of sprites to hold a contiguous list of vertices.
22 *   It has some advantages than creating manually an array of similar sprites such as:
23 *   - Copies only once to the SpriteBatch, which searches for the texture index once. 
24 *   - Boundary checks once
25 *   - Makes the sprites array vertices linear, reducing cache misses.
26 *   - Wraps the setTexture and draw process so no need to manually execute the foreach
27 */
28 class HipMultiSprite
29 {
30     protected HipSpriteVertex[] vertices;
31     HipSprite[] sprites;
32     IHipTexture texture;
33     this(size_t spritesCount)
34     {
35         vertices = new HipSpriteVertex[4*spritesCount];
36         sprites = new HipSprite[spritesCount];
37         foreach(i; 0..spritesCount)
38             sprites[i] = new HipSprite(vertices[i*4..(i+1)*4]);
39     }
40 
41     ref HipSprite opIndex(size_t index){return sprites[index];}
42 
43     int opApply(scope int delegate(ref HipSprite) dg)
44     {
45         int result = 0;
46         foreach (item; sprites)
47         {
48             result = dg(item);
49             if (result)
50                 break;
51         }
52         return result;
53     }
54 
55     
56     int opApply(scope int delegate(size_t index, ref HipSprite) dg)
57     {
58         int result = 0;
59         foreach (i, item; sprites)
60         {
61             result = dg(i, item);
62             if (result)
63                 break;
64         }
65         return result;
66     }
67 
68 
69     void setTexture(IHipTexture texture)
70     {
71         this.texture = texture;
72         foreach(sp; sprites)
73             sp.setTexture(texture);
74     }
75 
76     ref HipSpriteVertex[] getVertices()
77     {
78         //Vertices is already a data sink for the sprites, so no need to reassign.
79         foreach(i, sp; sprites)
80             sp.getVertices;
81         return vertices;
82     }
83 
84     void draw()
85     {
86         import hip.api.graphics.g2d.renderer2d;
87         foreach(sp; sprites)
88             sp.isDirty = true;
89         drawSprite(texture, cast(ubyte[])getVertices());
90     }
91 }
92 
93 class HipSprite 
94 {
95     IHipTextureRegion texture;
96     HipColor color = HipColor.white;
97     float x = 0, y = 0;
98     float scrollX = 0, scrollY = 0;
99     float rotation = 0;
100     //Tiling == 1 for consistency, 0 the image would disappear
101     float tilingX = 1, tilingY = 1;
102     float scaleX = 1, scaleY = 1;
103 
104     float u1 = 0, v1 = 0, u2 = 0, v2 = 0;
105 
106     ///Width of the texture region, (u2-u1) * texture.width
107     uint width;
108     ///Height of the texture region, (v2-v1) * texture.height
109     uint height;
110 
111     private bool flippedX, flippedY;
112 
113     protected bool isDirty = true;
114     protected HipSpriteVertex[] vertices;
115 
116     package this(HipSpriteVertex[] sink)
117     {
118         this.vertices = sink;
119         setColor(HipColor.white);
120     }
121 
122     this()
123     {
124         vertices = new HipSpriteVertex[4];
125         setColor(HipColor.white);
126         setTexture(cast()HipDefaultAssets.getDefaultTexture());
127     }
128     this(IHipAssetLoadTask task)
129     {
130         this();
131         setTexture(task);
132     }
133 
134     /**
135     *   Synchronous texture loading based on a string.
136     */
137     this(string texturePath)
138     {
139         this(HipAssetManager.createTextureRegion(HipAssetManager.loadTexture(texturePath).awaitAs!IHipTexture));
140     }
141 
142     this(IHipTexture texture)
143     {
144         vertices = new HipSpriteVertex[4];
145         setTexture(texture);
146     }
147     this(IHipTextureRegion region)
148     {
149         vertices = new HipSpriteVertex[4];
150         this.texture = region;
151         width  = region.getWidth();
152         height = region.getHeight();
153         setRegion(region.getRegion());
154     }
155 
156     void setTexture(IHipTexture texture)
157     {
158         import hip.api;
159         this.texture = HipAssetManager.createTextureRegion(texture);
160         width  = texture.getWidth;
161         height = texture.getHeight;
162         setRegion(this.texture.getRegion());
163     }
164     void setTexture(IHipAssetLoadTask task)
165     {
166         HipAssetManager.addOnCompleteHandler(task, (asset)
167         {
168             this.setTexture(cast(IHipTexture)asset);
169         });
170     }
171 
172     final IHipTexture getTexture() { return texture.getTexture();}
173     final void setRegion(float u1, float v1, float u2, float v2)
174     {
175         setRegion(TextureCoordinatesQuad(u1,v1,u2,v2));
176     }
177     void setRegion(IHipTextureRegion region)
178     {
179         width = region.getWidth();
180         height = region.getHeight();
181         texture = region;
182         setRegion(region.getRegion());
183     }
184     void setRegion(TextureCoordinatesQuad c)
185     {
186         this.u1 = c.u1;
187         this.u2 = c.u2;
188         this.v1 = c.v1;
189         this.v2 = c.v2;
190         texture.setRegion(c.u1, c.v1, c.u2, c.v2);
191         const float[] v = texture.getVertices();
192 
193         vertices[0].vTexST = Vector2(v[0], v[1]);
194         vertices[1].vTexST = Vector2(v[2], v[3]);
195         vertices[2].vTexST = Vector2(v[4], v[5]);
196         vertices[3].vTexST = Vector2(v[6], v[7]);
197         if(flippedX)
198         {
199             flippedX = false;
200             setFlippedX(true);
201         }
202         if(flippedY)
203         {
204             flippedY = false;
205             setFlippedY(true);
206         }
207     }
208 
209     void setPosition(float x, float y)
210     {
211         this.x = x;
212         this.y = y;
213 
214         if(isDirty)return;
215 
216         if(rotation != 0 || scaleX != 1 || scaleY != 1)
217         {
218             isDirty = true;
219             return;
220         }
221 
222         float x2 = x+width;
223         float y2 = y+height;
224 
225         //Top left
226         vertices[0].vPosition = Vector3(x, y,0);
227 
228         //Top right
229         vertices[1].vPosition = Vector3(x2, y,0);
230 
231         //Bot right
232         vertices[2].vPosition = Vector3(x2, y2,0);
233 
234         //Bot left
235         vertices[3].vPosition = Vector3(x, y2,0);
236     }
237 
238     ref HipSpriteVertex[] getVertices()
239     {
240         if(isDirty)
241         {
242             isDirty = false;
243             float _x = -cast(float)width/2 * scaleX;
244             float _y = -cast(float)height/2 * scaleY;
245             float x2 = _x+(width * scaleX);
246             float y2 = _y+(height * scaleY); 
247             if(rotation == 0)
248             {
249                 //Top left
250                 vertices[0].vPosition = Vector3(_x+x, _y+y,0);
251 
252                 //Top right
253                 vertices[1].vPosition = Vector3(x2+x, _y+y,0);
254 
255                 //Bot right
256                 vertices[2].vPosition = Vector3(x2+x, y2+y,0);
257 
258                 //Bot left
259                 vertices[3].vPosition = Vector3(_x+x, y2+y,0);
260             }
261             else
262             {
263                 import core.math:sin,cos;
264                 float c = cos(rotation);
265                 float s = sin(rotation);
266 
267                 //Top left
268                 vertices[0].vPosition = Vector3(c*_x - s*_y + this.x, c*_y + s*_x + this.y,0);
269 
270                 //Top right
271                 vertices[1].vPosition = Vector3(c*x2 - s*_y + this.x, c*_y + s*x2 + this.y,0);
272 
273                 //Bot right
274                 vertices[2].vPosition = Vector3(c*x2 - s*y2 + this.x, c*y2 + s*x2 + this.y,0);
275 
276                 //Bot left
277                 vertices[3].vPosition = Vector3(c*_x - s*y2 + this.x, c*y2 + s*_x + this.y,0);
278             }
279         }
280         return vertices;
281     }
282 
283     void setColor(HipColor color)
284     {
285         this.color = color;
286         vertices[0].vColor = color;
287         vertices[1].vColor = color;
288         vertices[2].vColor = color;
289         vertices[3].vColor = color;
290     }
291 
292     void setScale(float scaleX, float scaleY)
293     {
294         this.scaleX = scaleX;
295         this.scaleY = scaleY;
296         isDirty = true;
297     }
298     void setRotation(float rotation)
299     {
300         import hip.math.utils;
301         this.rotation = rotation % (PI * 2);
302         isDirty = true;
303     }
304     ///Same thing as setRotation, but receives in Degrees
305     void setAngle(float angle)
306     {
307         import hip.math.utils:degToRad;
308         setRotation(degToRad(angle));
309     }
310 
311     int getWidth() const {return width;}
312     int getHeight() const {return height;}
313     int getTextureWidth() const {return texture.getTextureWidth();}
314     int getTextureHeight() const {return texture.getTextureHeight();}
315 
316     /**
317     * This function is most useful for single images. For instance backgrounds, probably, if you have a
318     * texture atlas or a spritesheet, this function is not useful
319     */
320     void setScroll(float x, float y)
321     {
322         setRegion(
323             -scrollX + x + u1,
324             -scrollY + y + v1,
325             -scrollX + x + u2,
326             -scrollY + y + v2
327         );
328         scrollX = x;
329         scrollY = y;
330     }
331 
332     void setFlippedX(bool flip)
333     {
334         if(flip != flippedX)
335         {
336             auto reg = texture.getRegion;
337             flippedX = flip;
338             vertices[0].vTexST.x = flip ? reg.u2 : reg.u1;
339             vertices[1].vTexST.x = flip ? reg.u1 : reg.u2;
340             vertices[2].vTexST.x = flip ? reg.u1 : reg.u2;
341             vertices[3].vTexST.x = flip ? reg.u2 : reg.u1;
342         }
343     }
344     void setFlippedY(bool flip)
345     {
346         if(flip != flippedY)
347         {
348             auto reg = texture.getRegion;
349             flippedY = flip;
350             vertices[0].vTexST.y = flip ? reg.v2 : reg.v1;
351             vertices[1].vTexST.y = flip ? reg.v2 : reg.v1;
352             vertices[2].vTexST.y = flip ? reg.v1 : reg.v2;
353             vertices[3].vTexST.y = flip ? reg.v1 : reg.v2;
354         }
355     }
356     bool isFlippedX() => flippedX;
357     bool isFlippedY() => flippedY;
358 
359 
360     /**
361     *   Sets the tiling factor for this sprite. Default is 1.
362     */
363     void setTiling(float x = 1, float y = 1)
364     {
365         assert(x != 0 && y != 0, "Tiling factor equals 0 will disappear the sprite image");
366 
367         setRegion(
368             u1 / tilingX * x,
369             v1 / tilingY * y,
370             u2 / tilingX * x,
371             v2 / tilingY * y
372         );
373         tilingX = x;
374         tilingY = y;
375     }
376 
377     void draw()
378     {
379         import hip.api.graphics.g2d.renderer2d;
380         this.isDirty = true;
381         drawSprite(texture.getTexture, cast(ubyte[])getVertices[]);
382     }
383 }
384 
385 
386 class HipSpriteAnimation : HipSprite
387 {
388     import hip.api.graphics.g2d.animation;
389     private IHipAnimation animation;
390     HipAnimationFrame* currentFrame;
391 
392     this(){super();}
393 
394     this(IHipAnimation anim)
395     {
396         super();
397         animation = anim;
398         this.setAnimation(anim.getCurrentTrackName());
399     }
400 
401     IHipAnimationTrack getAnimation(string animName)
402     {
403         return animation.getTrack(animName);
404     }
405     /**
406     *   Sets internal animation data.
407     */
408     void setAnimation(IHipAnimation anim)
409     {
410         animation = anim;
411         setAnimation(animation.getCurrentTrackName());
412     }
413     void setAnimation(string animName)
414     {
415         animation.play(animName);
416         setFrame(animation.getCurrentFrame());
417     }
418 
419     void setBounds(int width, int height)
420     {
421         this.width = width;
422         this.height = height;
423     }
424 
425     void setFrame(HipAnimationFrame* frame)
426     {
427         this.currentFrame = frame;
428         this.texture = frame.region;
429         setBounds(frame.region.getWidth(), frame.region.getHeight());
430         setRegion(texture.getRegion());
431     }
432 
433     void update(float dt)
434     {
435         animation.update(dt);
436         setFrame(animation.getCurrentFrame());
437     }
438 }